home *** CD-ROM | disk | FTP | other *** search
/ PC World Komputer 2010 April / PCWorld0410.iso / hity wydania / Ubuntu 9.10 PL / karmelkowy-koliberek-9.10-netbook-remix-PL.iso / casper / filesystem.squashfs / usr / share / pyshared / glchess / ai.py < prev    next >
Text File  |  2009-09-22  |  14KB  |  493 lines

  1. # -*- coding: utf-8 -*-
  2. __author__ = 'Robert Ancell <bob27@users.sourceforge.net>'
  3. __license__ = 'GNU General Public License Version 2'
  4. __copyright__ = 'Copyright 2005-2006  Robert Ancell'
  5.  
  6. import os
  7. import sys
  8. import select
  9. import signal
  10. import xml.dom.minidom
  11. import xml.parsers.expat
  12.  
  13. import game
  14. import cecp
  15. import uci
  16.  
  17. from defaults import *
  18.  
  19. CECP = 'CECP'
  20. UCI  = 'UCI'
  21.  
  22. class Option:
  23.     """
  24.     """
  25.  
  26.     value = ''
  27.  
  28. class Level:
  29.     """
  30.     """
  31.  
  32.     def __init__(self):
  33.         self.options = []
  34.  
  35. class Profile:
  36.     """
  37.     """
  38.  
  39.     def __init__(self):
  40.         self.name = ''
  41.         self.protocol = ''
  42.         self.path = ''
  43.         self.executables = []
  44.         self.arguments = []
  45.         self.profiles = {}
  46.  
  47.     def detect(self):
  48.         """
  49.         """
  50.         try:
  51.             path = os.environ['PATH'].split(os.pathsep)
  52.         except KeyError:
  53.             path = []
  54.  
  55.         for directory in path:
  56.             for executable in self.executables:
  57.                 b = directory + os.sep + executable
  58.                 if os.path.isfile(b):
  59.                     self.path = b
  60.                     return
  61.         self.path = None
  62.         
  63. def _getXMLText(node):
  64.     """
  65.     """
  66.     if len(node.childNodes) == 0:
  67.         return ''
  68.     if len(node.childNodes) > 1 or node.childNodes[0].nodeType != node.TEXT_NODE:
  69.         raise ValueError
  70.     return node.childNodes[0].nodeValue
  71.  
  72. def _loadLevel(node):
  73.     """
  74.     """
  75.     level = Level()
  76.     n = node.getElementsByTagName('name')
  77.     if len(n) != 1:
  78.         return None
  79.     level.name = _getXMLText(n[0])
  80.     
  81.     for e in node.getElementsByTagName('option'):
  82.         option = Option()
  83.         option.value = _getXMLText(e)
  84.         try:
  85.             attribute = e.attributes['name']
  86.         except KeyError:
  87.             pass
  88.         else:
  89.             option.name = _getXMLText(attribute)
  90.         level.options.append(option)
  91.         
  92.     return level
  93.  
  94. def loadProfiles():
  95.     """
  96.     """
  97.     profiles = []
  98.     
  99.     fileNames = [os.path.expanduser(LOCAL_AI_CONFIG), os.path.join(BASE_DIR, 'ai.xml'), 'ai.xml']
  100.     document = None
  101.     for f in fileNames:
  102.         try:
  103.             document = xml.dom.minidom.parse(f)
  104.         except IOError:
  105.             pass
  106.         except xml.parsers.expat.ExpatError:
  107.             print 'AI configuration from %s is invalid, ignoring' % f
  108.         else:
  109.             #print 'Loading AI configuration from %s' % f
  110.             break
  111.     if document is None:
  112.         print 'WARNING: No AI configuration'
  113.         return profiles
  114.  
  115.     elements = document.getElementsByTagName('aiconfig')
  116.     if len(elements) == 0:
  117.         return profiles
  118.  
  119.     for e in elements:
  120.         for p in e.getElementsByTagName('ai'):
  121.             try:
  122.                 protocolName = p.attributes['type'].nodeValue
  123.             except KeyError:
  124.                 assert(False)
  125.             if protocolName == 'cecp':
  126.                 protocol = CECP
  127.             elif protocolName == 'uci':
  128.                 protocol = UCI
  129.             else:
  130.                 assert(False), 'Uknown AI type: %s' % repr(protocolName)
  131.             
  132.             n = p.getElementsByTagName('name')
  133.             assert(len(n) > 0)
  134.             name = _getXMLText(n[0])
  135.  
  136.             executables = []
  137.             n = p.getElementsByTagName('binary')
  138.             assert(len(n) > 0)
  139.             for x in n:
  140.                 executables.append(_getXMLText(x))
  141.  
  142.             arguments = []
  143.             for x in p.getElementsByTagName('argument'):
  144.                 arguments.append(_getXMLText(x))
  145.  
  146.             levels = {}
  147.             for x in p.getElementsByTagName('level'):
  148.                 level = _loadLevel(x)
  149.                 if level is not None:
  150.                     levels[level.name] = level
  151.  
  152.             profile = Profile()
  153.             profile.name        = name
  154.             profile.protocol    = protocol
  155.             profile.executables = executables
  156.             profile.arguments   = arguments
  157.             profile.levels      = levels
  158.             profiles.append(profile)
  159.     
  160.     return profiles
  161.  
  162. class CECPConnection(cecp.Connection):
  163.     """
  164.     """
  165.  
  166.     def __init__(self, player):
  167.         """
  168.         """
  169.         self.player = player
  170.         cecp.Connection.__init__(self)
  171.         
  172.     def onOutgoingData(self, data):
  173.         """Called by cecp.Connection"""
  174.         self.player.logText(data, 'output')
  175.         self.player.sendToEngine(data)
  176.  
  177.     def onMove(self, move):
  178.         """Called by cecp.Connection"""
  179.         if self.player.isReadyToMove():
  180.             self.player.moving = True
  181.             self.player.move(move)
  182.         else:
  183.             assert(self.player.suppliedMove is None)
  184.             self.player.suppliedMove = move
  185.  
  186.     def onResign(self):
  187.         """Called by cecp.Connection"""
  188.         self.player.resign()
  189.  
  190.     def onDraw(self):
  191.         """Called by cecp.Connection"""
  192.         print self.player.claimDraw()
  193.  
  194.     def logText(self, text, style):
  195.         """Called by cecp.Connection"""
  196.         self.player.logText(text, style)
  197.  
  198. class UCIConnection(uci.StateMachine):
  199.     """
  200.     """
  201.  
  202.     def __init__(self, player):
  203.         """
  204.         """
  205.         self.player = player
  206.         uci.StateMachine.__init__(self)
  207.         
  208.     def onOutgoingData(self, data):
  209.         """Called by uci.StateMachine"""
  210.         self.player.logText(data, 'output')
  211.         self.player.sendToEngine(data)
  212.         
  213.     def logText(self, text, style):
  214.         """Called by uci.StateMachine"""
  215.         self.player.logText(text, style)
  216.  
  217.     def onMove(self, move):
  218.         """Called by uci.StateMachine"""
  219.         self.player.move(move)
  220.  
  221. # Catch zombie processes
  222. def _cDied(sig, stackFrame):
  223.     try:
  224.         (pid, status) = os.waitpid(-1, os.WNOHANG)
  225.     except OSError:
  226.         pass
  227. signal.signal(signal.SIGCHLD, _cDied)      
  228.  
  229. class Player(game.ChessPlayer):
  230.     """
  231.     """
  232.  
  233.     def __init__(self, name, profile, level = 'normal'):
  234.         """Constructor for an AI player.
  235.         
  236.         'name' is the name of the player (string).
  237.         'profile' is the profile to use for the AI (Profile).
  238.         'level' is the difficulty level to use (string).
  239.         """
  240.         self.__profile = profile
  241.         self.__level = level
  242.         
  243.         self.moving = False
  244.         self.suppliedMove = None
  245.  
  246.         game.ChessPlayer.__init__(self, name)
  247.         
  248.         # Pipe to communicate to engine with
  249.         (toManagerOutput, toManagerInput) = os.pipe()
  250.         (fromManagerOutput, fromManagerInput) = os.pipe()
  251.         
  252.         # Store the file descripter for reading/writing
  253.         self.__toEngineFd = toManagerInput
  254.         self.__fromEngineFd = fromManagerOutput
  255.  
  256.         # Fork off a child process to manage the engine
  257.         try:
  258.             self.__pid = os.fork()
  259.         except OSError, e:
  260.             print 'Monitor failed to fork: %s' % e.message
  261.             os.close(toManagerInput)
  262.             os.close(toManagerOutput)
  263.             os.close(fromManagerInput)
  264.             os.close(fromManagerOutput)
  265.             self.__toEngineFd = None
  266.             self.__fromEngineFd = None
  267.         else:
  268.             if self.__pid == 0:
  269.                 os.close(toManagerInput)
  270.                 os.close(fromManagerOutput)
  271.                 self._runMonitor(fromManagerInput, toManagerOutput)
  272.                 os.close(toManagerOutput)
  273.                 os.close(fromManagerInput)
  274.                 os._exit(0)
  275.  
  276.             os.close(toManagerOutput)
  277.             os.close(fromManagerInput)
  278.  
  279.             if profile.protocol == CECP:
  280.                 self.connection = CECPConnection(self)
  281.             elif profile.protocol == UCI:
  282.                 self.connection = UCIConnection(self)
  283.             else:
  284.                 assert(False)
  285.             
  286.             self.connection.start()
  287.             self.connection.startGame()
  288.             try:
  289.                 level = self.__profile.levels[self.__level]
  290.             except KeyError:
  291.                 self.connection.configure()
  292.             else:
  293.                 self.connection.configure(level.options)
  294.  
  295.     # Methods to extend
  296.     
  297.     def logText(self, text, style):
  298.         """
  299.         """
  300.         pass
  301.         
  302.     # Public methods
  303.     
  304.     def getProfile(self):
  305.         """Get the AI profile this AI is using.
  306.         
  307.         Returns a 2-tuple containing the profile name (str) and the difficulty level (str).
  308.         """
  309.         return (self.__profile.name, self.__level)
  310.     
  311.     def fileno(self):
  312.         """Returns the file descriptor for communicating with the engine (integer)"""
  313.         return self.__fromEngineFd
  314.  
  315.     def read(self):
  316.         """Read and process data from the engine.
  317.         
  318.         This is non-blocking.
  319.         """       
  320.         while True:
  321.             # Connection was closed
  322.             if self.__fromEngineFd == None:
  323.                 return False
  324.  
  325.             # Check if data is available
  326.             (rlist, _, xlist) = select.select([self.__fromEngineFd], [], [self.__fromEngineFd], 0)
  327.             if (len(rlist) + len(xlist)) == 0:
  328.                 return True
  329.  
  330.             # Read a chunk and process
  331.             try:
  332.                 data = os.read(self.__fromEngineFd, 256)
  333.             except OSError, e:
  334.                 print 'Error reading from chess engine: ' + str(e)
  335.                 self._die()
  336.                 return False
  337.             if len(data) == 0:
  338.                 print 'Engine has died'
  339.                 self._die()
  340.                 return False
  341.             self.connection.registerIncomingData(data)
  342.  
  343.     def sendToEngine(self, data):
  344.         """
  345.         """
  346.         if self.__toEngineFd == None:
  347.             return
  348.         
  349.         try:
  350.             os.write(self.__toEngineFd, data)
  351.         except OSError, e:
  352.             print 'Failed to write to engine: ' + str(e)
  353.  
  354.     def quit(self):
  355.         """Disconnect the AI"""
  356.         fd = self.__toEngineFd
  357.         self.__toEngineFd = None
  358.         self.__fromEngineFd = None
  359.         
  360.         # Send quit
  361.         try:
  362.             if fd is not None:
  363.                 os.write(fd, '\nquit\n') # FIXME: CECP specific
  364.         except OSError:
  365.             return
  366.  
  367.     # Extended methods
  368.     
  369.     def onPlayerMoved(self, player, move):
  370.         """Called by game.ChessPlayer"""
  371.         if self.__toEngineFd == None:
  372.             return
  373.         isSelf = player is self and self.moving
  374.         self.moving = False
  375.         self.connection.reportMove(move.canMove, isSelf)
  376.         
  377.     def onUndoMove(self):
  378.         """Called by game.ChessPlayer"""
  379.         self.connection.undoMove()
  380.  
  381.     def readyToMove(self):
  382.         """Called by game.ChessPlayer"""
  383.         if self.__toEngineFd == None:
  384.             self.die()
  385.             return
  386.  
  387.         # AI Engines always claim draw due to three-fold-repetition and
  388.         # 50 move rule
  389.         if self.claimDraw():
  390.             return
  391.         
  392.         game = self.getGame()
  393.         whiteTime = game.getWhite().getRemainingTime()
  394.         blackTime = game.getBlack().getRemainingTime()
  395.         if game.getWhite() is self:
  396.             ownTime = whiteTime
  397.         else:
  398.             ownTime = blackTime
  399.         
  400.         if self.suppliedMove is None:
  401.             self.connection.requestMove(whiteTime, blackTime, ownTime)
  402.         else:
  403.             self.moving = True
  404.             move = self.suppliedMove
  405.             self.suppliedMove = None
  406.             self.move(move)
  407.  
  408.             # AI Engines always claim draw due to three-fold-repetition and
  409.             # 50 move rule
  410.             self.claimDraw()
  411.  
  412.     def onGameEnded(self, game):
  413.         """Called by game.ChessPlayer"""
  414.         self.quit()
  415.         
  416.     def _die(self):
  417.         self.quit()
  418.         self.die()
  419.  
  420.     def _runEngine(self, toEngineFd, fromEngineFd):
  421.         # Make the engine low priority for CPU usage
  422.         os.nice(19)
  423.                 
  424.         # Change directory so any log files are not in the users home directory
  425.         try:
  426.             os.mkdir(LOG_DIR)
  427.         except OSError:
  428.             pass
  429.         try:
  430.             os.chdir(LOG_DIR)
  431.         except OSError:
  432.             pass
  433.                 
  434.         # Connect stdin, stdout and stderr to the manager process
  435.         os.dup2(toEngineFd, sys.stdin.fileno())
  436.         os.dup2(fromEngineFd, sys.stdout.fileno())
  437.         os.dup2(fromEngineFd, sys.stderr.fileno())
  438.                 
  439.         # Execute the engine
  440.         try:
  441.             os.execv(self.__profile.path, [self.__profile.path] + self.__profile.arguments)
  442.         except OSError:
  443.             pass
  444.  
  445.     def _runMonitor(self, toApplicationFd, fromApplicationFd):
  446.         # Make pipes to the child process
  447.         (toEngineOutput, toEngineInput) = os.pipe()
  448.         (fromEngineOutput, fromEngineInput) = os.pipe()
  449.  
  450.         # Fork and execute the child
  451.         try:
  452.             enginePID = os.fork()
  453.         except OSError, e:
  454.             print 'Monitor failed to fork: %s' % e.message
  455.             os._exit(1);
  456.         if enginePID == 0:
  457.             os.close(toApplicationFd)
  458.             os.close(fromApplicationFd)            
  459.             os.close(toEngineInput)
  460.             os.close(fromEngineOutput)
  461.             self._runEngine(toEngineOutput, fromEngineInput)
  462.             os._exit(0)
  463.         else:
  464.             os.close(toEngineOutput)
  465.             os.close(fromEngineInput)
  466.  
  467.         # Forward data between the application and the child process and wait for closed pipes
  468.         inputPipes = (fromApplicationFd, fromEngineOutput)
  469.         targets = {fromApplicationFd: toEngineInput,
  470.                    fromEngineOutput: toApplicationFd}
  471.         pipes = (toApplicationFd, fromApplicationFd,
  472.                  toEngineInput, fromEngineOutput)
  473.                  
  474.         try:
  475.             while True:
  476.                 # Wait for data
  477.                 (rfds, _, xfds) = select.select(inputPipes, [], pipes, None)
  478.                 
  479.                 for fd in rfds:
  480.                     data = os.read(fd, 65535)
  481.                     if len(data) == 0:
  482.                         raise OSError('End of data')
  483.                 
  484.                     # Bridge information between the application and the engine
  485.                     os.write(targets[fd], data)
  486.         except:
  487.             # Kill the child and exit
  488.             try:
  489.                 os.kill(enginePID, signal.SIGQUIT)
  490.             except OSError:
  491.                 pass
  492.             os._exit(0)
  493.